<?php
namespace UCenter;

use \MediaWiki\Auth\AuthenticationRequest;
use \MediaWiki\Auth\ButtonAuthenticationRequest;
use \MediaWiki\Auth\AbstractPrimaryAuthenticationProvider;
use \MediaWiki\Auth\AbstractPasswordPrimaryAuthenticationProvider;
use \MediaWiki\Auth\PasswordAuthenticationRequest;
use \MediaWiki\Auth\UserDataAuthenticationRequest;
use \MediaWiki\Auth\AuthManager;
use \MediaWiki\Auth\AuthenticationResponse;
use MediaWiki\MediaWikiServices;
use \User;

class UCenterAuth extends AbstractPasswordPrimaryAuthenticationProvider {

	/** @var bool If true, this instance is for legacy logins only. */
	protected $loginOnly = false;
	protected $uc;

	/**
	 * @param array $params Settings
	 *  - loginOnly: If true, the local passwords are for legacy logins only:
	 *    the local password will be invalidated when authentication is changed
	 *    and new users will not have a valid local password set.
	 */
	public function __construct($params = []){
		parent::__construct($params);
		$this->loginOnly = !empty($params['loginOnly']);
		$this->uc = new UCenter();
	}
	
	protected function updateOrCreateRemoteUser($username, $password, $email = ''){
		$user = User::newFromName($username);
		$ret = $this->uc->uc_user_login($username, $password);
		switch($ret[0]){
			case -1: //用户不存在，创建用户
				if(empty($email)){
					//记得修改这里的email后缀
					$email = $this->punycodeEnc($username) . '@nomail.com';
				}
				$this->uc->uc_user_register($username, $password, $email);
				return -1;
			case -2: //密码错误，更新密码
				$this->uc->uc_user_edit($username, null, $password, null, 1);
				return -2;
			default:
				return 1;
		}
	}
	
	protected function punycodeEnc($str){
		$punycode = new PunyCode();
		return $punycode->encode($str);
	}
	
	protected function createRemoteUser($username, $password, $email = ''){
		if(empty($email)){
			$user = User::newFromName($username);
			$email = $user->getEmail();
			if(empty($email)){
				//记得修改这里的email地址后缀
				$email = $this->punycodeEnc($username) . '@nomail.com';
			}
		}
		$uc = new UCenter();
		return $this->uc->uc_user_register($username, $password, $email);
	}
	
	protected function remoteLogin($username, $password){
		$uc = new UCenter();
		return $this->uc->uc_user_login($username, $password)[0];
	}
	
	protected function hasRemoteUser($username){
		list($uid, $drop1, $drop2)= $this->uc->uc_get_user($username);
		return $uid > 0;
	}
	
	protected function getSyncLogin($username){
		$uc = new UCenter();
		list($uid, $username, $email)= $uc->uc_get_user($username);
		return $this->uc->uc_user_synlogin($uid);
	}

	protected function getPasswordResetData($username, $row){
		$now = wfTimestamp();
		$expiration = wfTimestampOrNull(TS_UNIX, $row->user_password_expires);
		if($expiration === null || $expiration >= $now){
			return null;
		}

		$grace = $this->config->get('PasswordExpireGrace');
		if($expiration + $grace < $now){
			$data = [
				'hard' => true,
				'msg' => \Status::newFatal('resetpass-expired')->getMessage(),
			];
		} else {
			$data = [
				'hard' => false,
				'msg' => \Status::newFatal('resetpass-expired-soft')->getMessage(),
			];
		}

		return(object)$data;
	}

	public function beginPrimaryAuthentication(array $reqs){
		$service = MediaWikiServices::getInstance();
		$req = AuthenticationRequest::getRequestByClass($reqs, PasswordAuthenticationRequest::class);
		$usernameUtils = $service->getUserNameUtils();
		if(!$req){
			return AuthenticationResponse::newAbstain();
		}

		if($req->username === null || $req->password === null){
			return AuthenticationResponse::newAbstain();
		}
		
		$username = $usernameUtils->getCanonical($req->username, $usernameUtils::RIGOR_CREATABLE);
		$password = $req->password;
		
		$status = (int)$this->remoteLogin($username, $password); //远程登录
		if($status > 0){ //登录成功
			$user = User::newFromName($username, 'usable');
			if($user->getId() !== 0){ //用户存在
				$status = $user->checkPasswordValidity($password);
				if(!$status->isOk()){ //需要更新密码
					$manager = $service->getAuthManager();
					$req = new PasswordAuthenticationRequest();
					$req->username = $username;
					$req->password = $password;
					$req->retype = $password;
					$req->action = 'change';
					$req->required = 2;
					$manager->changeAuthenticationData($req);
				}
			} elseif($this->manager->userCanAuthenticate( $username )
				|| ( !$user->getEmail() && $user->getToken() !== $user->getToken() ) ) {
				//解决1.35新创建的用户无法登录的问题
				return AuthenticationResponse::newAbstain();
			}
			return AuthenticationResponse::newPass($username);
		} elseif($status === -2){ //密码错误
			return AuthenticationResponse::newFail(wfMessage('wrongpassword'));
		} elseif($status === -1){ //用户不存在，先用本地密码验证，再同步到uc
			$username = $usernameUtils->getCanonical($username, $usernameUtils::RIGOR_USABLE);
			if($username === false){
				return AuthenticationResponse::newAbstain();
			}
			$status = $this->checkPasswordValidity($username, $password);
			if(!$status->isOK()){
				// Fatal, can't log in
				return AuthenticationResponse::newFail($status->getMessage());
			}
			$this->createRemoteUser($username, $password);
			return AuthenticationResponse::newAbstain();
		}
		return AuthenticationResponse::newAbstain();
	}

	public function testUserCanAuthenticate($username){
		if($this->hasRemoteUser($username)){
			return true;
		}
		$usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
		$username = $usernameUtils->getCanonical($username, $usernameUtils::RIGOR_USABLE);
		if($username === false){
			return false;
		}

		$dbr = wfGetDB(DB_REPLICA);
		$row = $dbr->selectRow(
			'user',
			['user_password'],
			['user_name' => $username],
			__METHOD__
		);
		if(!$row){
			return false;
		}

		// Check for *really* old password hashes that don't even have a type
		// The old hash format was just an md5 hex hash, with no type information
		if(preg_match('/^[0-9a-f]{32}$/', $row->user_password)){
			return true;
		}

		return !$this->getPassword($row->user_password)instanceof \InvalidPassword;
	}

	public function testUserExists($username, $flags = User::READ_NORMAL){
		if($this->hasRemoteUser($username)){
			return true;
		}
		$usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
		$username = $usernameUtils->getCanonical($username, $usernameUtils::RIGOR_USABLE);
		if($username === false){
			return false;
		}

		list($db, $options)= \DBAccessObjectUtils::getDBOptions($flags);
		return (bool)wfGetDB($db)->selectField(
			['user'],
			'user_id',
			['user_name' => $username],
			__METHOD__,
			$options
		);
	}

	public function providerAllowsAuthenticationDataChange(
		AuthenticationRequest $req, $checkData = true
	){
		// We only want to blank the password if something else will accept the
		// new authentication data, so return 'ignore' here.
		if($this->loginOnly){
			return \StatusValue::newGood('ignored');
		}

		if(get_class($req)=== PasswordAuthenticationRequest::class){
			if(!$checkData){
				return \StatusValue::newGood();
			}

			$usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
			$username = $usernameUtils->getCanonical($req->username, $usernameUtils::RIGOR_USABLE);
			if($username !== false){
				$row = wfGetDB(DB_MASTER)->selectRow(
					'user',
					['user_id'],
					['user_name' => $username],
					__METHOD__
				);
				if($row){
					$sv = \StatusValue::newGood();
					if($req->password !== null){
						if($req->password !== $req->retype){
							$sv->fatal('badretype');
						} else {
							$sv->merge($this->checkPasswordValidity($username, $req->password));
						}
					}
					return $sv;
				}
			}
		}

		return \StatusValue::newGood('ignored');
	}

	public function providerChangeAuthenticationData(AuthenticationRequest $req){
		$usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();
		$username = $req->username !== null ? $usernameUtils->getCanonical($req->username,
		$usernameUtils::RIGOR_USABLE): false;
		if($username === false){
			return;
		}

		$pwhash = null;

		$expiry = null;
		if(get_class($req)=== PasswordAuthenticationRequest::class){
			if($this->loginOnly){
				$pwhash = $this->getPasswordFactory()->newFromCiphertext(null);
			} else {
				$pwhash = $this->getPasswordFactory()->newFromPlaintext($req->password);
				$expiry = $this->getNewPasswordExpiry($username);
			}
		}

		if($pwhash){
			$dbw = wfGetDB(DB_MASTER);
			$dbw->update(
				'user',
				[
					'user_password' => $pwhash->toString(),
					'user_password_expires' => $dbw->timestampOrNull($expiry),
				],
				['user_name' => $username],
				__METHOD__
			);
		}
		$this->updateOrCreateRemoteUser($req->username, $req->password);
	}

	public function accountCreationType(){
		return $this->loginOnly ? self::TYPE_NONE : self::TYPE_CREATE;
	}

	public function testForAccountCreation($user, $creator, array $reqs){
		$req = AuthenticationRequest::getRequestByClass($reqs, PasswordAuthenticationRequest::class);

		$ret = \StatusValue::newGood();
		if(!$this->loginOnly && $req && $req->username !== null && $req->password !== null){
			if($req->password !== $req->retype){
				$ret->fatal('badretype');
			} else {
				$ret->merge(
					$this->checkPasswordValidity($user->getName(), $req->password)
				);
			}
		}
		return $ret;
	}

	public function beginPrimaryAccountCreation($user, $creator, array $reqs){
		if($this->accountCreationType()=== self::TYPE_NONE){
			throw new \BadMethodCallException('Shouldn\'t call this when accountCreationType()is NONE');
		}
		$usernameUtils = MediaWikiServices::getInstance()->getUserNameUtils();

		$req = AuthenticationRequest::getRequestByClass($reqs, PasswordAuthenticationRequest::class);
		$data = AuthenticationRequest::getRequestByClass($reqs, UserDataAuthenticationRequest::class);

		$username = $req->username !== null ? $usernameUtils->getCanonical($req->username,
			$usernameUtils::RIGOR_USABLE): false;
		if($username === false){
			return AuthenticationResponse::newFail(wfMessage('noname'));
		}
		$status = (int)$this->createRemoteUser($username, $req->password, $data->email);
		if($status > 0){
			if($req){
				if($req->username !== null && $req->password !== null){
					// Nothing we can do besides claim it, because the user isn't in
					// the DB yet
					if($req->username !== $user->getName()){
						$req = clone $req;
						$req->username = $user->getName();
					}
					$ret = AuthenticationResponse::newPass($req->username);
					$ret->createRequest = $req;
					return $ret;
				}
			}
		} else {
			switch($status){
				case -1:
				case -2:
					return AuthenticationResponse::newFail(wfMessage('password-login-forbidden'));
				case -3:
					return AuthenticationResponse::newFail(wfMessage('userexists'));
				case -4:
				case -5:
					return AuthenticationResponse::newFail(wfMessage('noemailcreate'));
				case -6:
					return AuthenticationResponse::newFail(wfMessage('useremailexists'));
			}
		}
		return AuthenticationResponse::newAbstain();
	}

	public function finishAccountCreation($user, $creator, AuthenticationResponse $res){
		if($this->accountCreationType()=== self::TYPE_NONE){
			throw new \BadMethodCallException('Shouldn\'t call this when accountCreationType()is NONE');
		}

		// Now that the user is in the DB, set the password on it.
		$this->providerChangeAuthenticationData($res->createRequest);

		return null;
	}
}
